LambdalithとSingle purpose Lambdaは1つのAPI Gatewayで共存できる

LambdalithとSingle purpose Lambdaは1つのAPI Gatewayで共存できる

Lambdalithな構成でサーバーレスアプリケーションを実装する事例が増えてきていると思います。実際に Lambdalith と Single purpose Lambda が1つの API Gateway の中で共存できるのか、CDKを用いて実装し試してみました。
Clock Icon2024.10.11

はじめに

最近、Monolith Lambda(以降 Lambdalith)な構成でサーバーレスアプリケーションを実装する事例が増えてきていると思います。

サーバーレスアプリケーションを作る際に、最初はLambdalithで構成し、必要になった場合に Single purpose Lambda と共存させれば良さそう、という意見が見られるようになりました。

今回は実際に Lambdalith と Single purpose Lambda が1つの API Gateway の中で共存できるのか、CDKを用いて実装し試してみました。

Single purpose LambdaとLambdalithの違い

Single purpose Lambda とは

lambdalith-single-purpose-lambda-in-one-api-gateway-1

APIのパス・メソッドとLambda が1対1で紐づく構成です。

数年前までこちらの構成が多かったと思います。

特定のパスのみアクセスが多い場合はメモリを増強して対応したり、特定のLambdaのみIAMポリシーでセキュアにしたり、といった個別Lambdaの対応ができる点がメリットです。

Lambdalith とは

lambdalith-single-purpose-lambda-in-one-api-gateway-2

複数パスのリクエストを1つのLambdaで捌く構成です。

Lambdaの中ではExpress.jsのようなフレームワークを利用してルーティングをします。

パスごとの細かい設定は出来なくなりますが、開発効率, コールドスタートの頻度軽減, コンテナ環境への移行のしやすさなどがメリットになります。

https://rehanvdm.com/blog/should-you-use-a-lambda-monolith-lambdalith-for-the-api

こちらのブログが詳しいので、ご参照ください。

CDKで実装する際にいろんなこと(リソース数上限, スタック分割, APIパスごとのリソース追加など)を考えなくて良くなるので、個人的にはLambdalithでの構成が好きです。

LambdalithとSingle purpose Lambdaの共存を試してみる

やりたいこと

lambdalith-single-purpose-lambda-in-one-api-gateway-3

やりたいことはシンプルです。

Lambdalithの構成に、Single purpose Lambdaを付け足すことができるのか検証していきます。

最初 Lambdalithで構成していたプロジェクトに、何らかの理由でSingle purpose Lambdaを使いたくなった時を想定しています。

CDKの実装

https://github.com/engineer-taro/lambdalith-single-coexistence

今回の実装はこちらのリポジトリにあります。

import {
  aws_apigateway,
  aws_lambda,
  aws_lambda_nodejs,
  aws_logs,
  RemovalPolicy,
  Stack,
  StackProps,
} from 'aws-cdk-lib';
import { Construct } from 'constructs';

export class LambdalithAndSinglePurposeLambdaStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const monolithLambda = new aws_lambda_nodejs.NodejsFunction(
      this,
      'MonolithLambda',
      {
        architecture: aws_lambda.Architecture.ARM_64,
        entry: 'src/lambdalith/entry-point.ts',
        runtime: aws_lambda.Runtime.NODEJS_20_X,
        bundling: {
          forceDockerBundling: false,
        },
      },
    );

    const singlePurposeLambda = new aws_lambda_nodejs.NodejsFunction(
      this,
      'SinglePurposeLambda',
      {
        architecture: aws_lambda.Architecture.ARM_64,
        entry: 'src/single-purpose-lambda/dog.ts',
        runtime: aws_lambda.Runtime.NODEJS_20_X,
        bundling: {
          forceDockerBundling: false,
        },
      },
    );

    /**
     * REST API を作成
     */
    const logGroup = new aws_logs.LogGroup(
      this,
      'MonolithSinglePurposeTogetherAccessLogs',
    );
    const restApi = new aws_apigateway.RestApi(
      this,
      'MonolithSinglePurposeTogether',
      {
        defaultIntegration: new aws_apigateway.LambdaIntegration(
          monolithLambda,
        ), // Lambdalithの設定
        defaultCorsPreflightOptions: {
          allowOrigins: aws_apigateway.Cors.ALL_ORIGINS,
          allowMethods: aws_apigateway.Cors.ALL_METHODS,
          allowHeaders: aws_apigateway.Cors.DEFAULT_HEADERS,
        },
        cloudWatchRole: true,
        cloudWatchRoleRemovalPolicy: RemovalPolicy.DESTROY,
        deployOptions: {
          accessLogDestination: new aws_apigateway.LogGroupLogDestination(
            logGroup,
          ),
          accessLogFormat: aws_apigateway.AccessLogFormat.clf(),
        },
      },
    );
    restApi.root.addProxy(); // Lambdalithの設定
    restApi.root
      .addResource('animals')
      .addResource('dog')
      .addMethod(
        'GET',
        new aws_apigateway.LambdaIntegration(singlePurposeLambda, {
          proxy: true,
        }),
      ); // Single purpose Lambdaの設定
  }
}

LambdalithのLambda実装

今回は @codegenie/serverless-express を利用して実装していきます。

import { injectLambdaContext, Logger } from "@aws-lambda-powertools/logger";
import serverlessExpress from "@codegenie/serverless-express";
import middy from "@middy/core";
import cors from "cors";
import express, { Request, Response } from "express";

const app = express();
app.use(cors());
app.use(express.json());
const logger = new Logger();

app.get(
  "/animals/cat",
  async (req: Request, res: Response): Promise<void> => {
    logger.info(req.path);
    const response = {
      statusCode: 200,
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ animal: "cat" }),
    }

    res.header(response.headers);
    res.status(response.statusCode).send(response.body);
  },
);

export const handler = middy(serverlessExpress({ app })).use(
  injectLambdaContext(logger),
);

Single Purpose LambdaのLamdba実装

こちらはAPIGatewayでproxy統合をする際の通常の実装です。

GET /animals/dog のパスで呼び出されるLambda関数です。

import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';

export const handler = async (
  event: APIGatewayProxyEvent,
): Promise<APIGatewayProxyResult> => {
  console.log(event);
  return {
    statusCode: 200,
    body: JSON.stringify({ animal: 'dog' }),
  };
};

動作確認

コンソール上ではこんな感じ。

lambdalith-single-purpose-lambda-in-one-api-gateway-4

curlコマンドで確認してみます。

まずは Single Purpose Lambda の実装がうまく呼び出せるか確認します。

curl https://sampleid.execute-api.ap-northeast-1.amazonaws.com/prod/animals/dog
{"animal":"dog"}

続いて、Lambdalithの実装がうまく呼び出せるか確認します。

curl https://sampleid.execute-api.ap-northeast-1.amazonaws.com/prod/animals/cat
{"animal":"cat"}

うまく共存できていますね。

小ネタ: LambdaRestApiのConstructを使うと共存できない

LambdaRestApi を利用して addResourceでパスを追加しようとすると、エラーが発生。CDKの実装的に、proxy: trueとなっていると新しいResourceを登録できない。

こちらの実装によるもの。

感想

「最初はLambdalithで実装して必要になったら Single Purpose Lambda を使う」という方法が本当に可能か気になったので検証してみました。

今回の検証で、「とりあえずLambdalith」への抵抗を減らすことができたので、同じことを検討している方の参考になれば幸いです。

(Lambdalithってめっちゃキャッチーだけど、Single purpose Lambda も同じくらい短く表現したい)

参考資料

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.